package util;
/*
 * SHAHelper class 
 * 1) Recursively walk through the target directory and all its subdirectories
 * 2) For all files and directories found, calculate SHA-1 values
 * 3) Store the key value pair <filepath, hashvalue> to a hashtable
 * 4) It's tested on the level of both directory and file: a) Any tiny changes of file content and attribute changes will
 *    be detected shown as completely different SHA-1. The attributes include file name, read, write, execute privileges of
 *    the owner of the file. Ability to detect other file attribute changes could be extended as requested. b) on the directory
 *    level, any above changes of the files or subdirectories in a specific directory will cause the complete change of its
 *    SHA-1 value. c) I've compare all above functions by copying the same set of files and directories across multiple place in the local drive 
 *    , modifying their contents and attributes in the same way. The same before-change and after change SHA-1 values are calculated.
 *     
 */
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import java.util.Map;


/**
 * 
 * @author lingqiluo <marsluo@bu.edu>
 * @version 1.2
 * @since 2012-03-26
 *
 */
public class SHAHelper {
 
	/**
	 * Hash Map to save the <file, hashvalue> value pairs of all the files under a specified directory
	 */
	public static Map<String,String> SHA_HashTable = new HashMap<String,String>();
	public static File HashFile = new File("myHashTable");
	
	/**
	 * Specified root directory.
	 */
	public static File rootDir;
	
	/**
	 * Arraylist to record all the directories while traversing downwards from the root directory
	 */
	public static ArrayList<String> directoryList = new ArrayList<String>();
	
	// the main method to test the SHAHelper
	// Step 1: set rootDir as the command argument 
	// Step 2: traverse all the files under the root directory, calculate all the SHA-1 values for all the files, 
	//         store them to 'SHA_HashTable'and record all the subdirectories
	// Step 3: Calculate SHA-1 values for all the subdirectories, also store them to 'SHA_HashTable'
    public static void main(String args[]) throws Exception  {
 
    	if (args.length == 0 || args.length > 2)
            usage();
    	
    	//directoryList = new ArrayList<String>();
    	
    	rootDir = new File(args[0]);
    	
    	// Initiate the directory traverse from the root directory and 
    	// Calculate the hashes for all the non-directory files and store them into the hash map
	    visitAllDirsAndFiles(rootDir);
	    
	    // At last, calculate the hashes for all the directories, also append them into the hash map
  	    ProcessFileSys();
  	    
  	    // Save the hash map to file
  	  SaveHashToFile(SHA_HashTable);
	    
  }
  
    static void usage() {
        System.err.println("usage: java SHAHelper dir");
        System.exit(-1);
    }
    
     
    /**
     * Traverse through directory
     *
     * Use {@link #process(File)} to calculate the hash values for all the surbordinate non-directory files.
     *
     * @param dir 		
     * 			  the root directory from which the application traverses recursively
     * @pre 
     * 			  dir should be in the java file format, not just file path string
     * @post
     * 			  The method traverse all the files&directories under the root directory recursively 
     * 		  	  and direct them to the method 'process(dir)' for processing
     * 			
     * 
     */ 
     public static void visitAllDirsAndFiles(File dir) throws Exception {
    	 
    	  if(dir.isFile() && !dir.isHidden())
    		  GetChecksum(dir);

    	  else if (dir.isDirectory()) { 
    		  directoryList.add(dir.getPath());
    		  System.out.println("directory: " + dir.getPath());
    		  
        	  String[] children = dir.list();
        	  for (int i=0; i<children.length; i++) {
        		  visitAllDirsAndFiles(new File(dir, children[i]));
        		  
        	  }
        	  
        	
          }
     }

     
     /**This method processes all the non-directory files under the root directory, calculate the SHA-1 values and 
      * save to a hashmap
      * 
      * 1) List all the file attributes that are required for tracking changes, including file name, owner's read
      *    write, execute privileges, file visibility and so on.
      * 2) Invoke a "SHA1" instance of MessageDigest class to process the content of a file byte by byte
      *    and a series of file attributes, return a string format of SHA-1 value for each file
      * 3) Print out the value pairs <filepath, hashvalue> on screen for all the qualified files
      * 4) Store all the value pairs into a data structure of hash map
      *
      * @param file 		the end file for SHA-1 value calculation
      * 
      * @pre 
      * 		file should be in the java file format, not just file path string
      * @post
      * 		The method performs identification of end files, exports the file contents to file stream,
      * 		calculation of the hash value for each file in the file stream, and storage of <file, hashvalue>
      * 		pair to Hash map
     * @return TODO
      * 
      */
     public static String GetChecksum(File file) throws Exception{
	
    	 	 //StringBuffer FileAttr = new StringBuffer("");

    	 	 //FileAttr.append(file.getName());
    	 	 //FileAttr.append(file.canRead());
    	 	 //FileAttr.append(file.canWrite());
    	 	 //FileAttr.append(file.canExecute());
    	 	// FileAttr.append(file.isHidden());
    	 	// FileAttr.append(file.lastModified());
    	 	 
   		 
    		 MessageDigest md = MessageDigest.getInstance("SHA1");
    		 
    		 //md.update(FileAttr.toString().getBytes()); //update the file attributes info
    		 		 
    		 FileInputStream fis = new FileInputStream(file);
    		 byte[] dataBytes = new byte[1024];
 
    		 int nread = 0; 
 
    		 while ((nread = fis.read(dataBytes)) != -1) {
    			 md.update(dataBytes, 0, nread);
    		 };
    		 
    		 byte[] mdbytes = md.digest();
 
    		 //convert the byte to hex format
    		 StringBuffer sb = new StringBuffer("");
    		 
    		 
    		 for (int i = 0; i < mdbytes.length; i++) {
    			 sb.append(Integer.toString((mdbytes[i] & 0xff) + 0x100, 16).substring(1));
    		 }
 
    		 System.out.println(file.getPath() + "------" + "Digest(in hex format):: " + sb.toString());
    
    		 // store the <filepath, hashvalue> pair
    		 SHA_HashTable.put(file.getPath(), sb.toString());
    		 
    		 return sb.toString();
   
     }
     
     /**
      * This method processes all the underlying directories under the root direcotry, calculate their SHA-1 values 
      * based on the hash values of the files in the specific directory (hash of hashes), and save the directory hashes 
      * to the same hashmap
      * 
      * 1) For each of the directory in the list, loop through the current hashmap, get and combine all the hash texts of 
      *    the children files and directories of the directory
      *    
      * 2) Invoke a "SHA1" instance of MessageDigest class, calculate the hash value for the above combined text, which is
      *    the SHA-1 value of the directories
      *    
      * 3) Store all the directory hashes to a temporary hashmap, and then mixing the file and directory hash values by 
      *    appending it to the 'SHA_HashTable'
      * 
      *
      * @pre 
      * 		directList should be an array list
     * @throws Exception 
      * 
      */
     public static void ProcessFileSys() throws Exception
     {
    	 int size = directoryList.size();
    	    
    	 if(size>0){
    		 Map<String,String> Temp_HashTable = new HashMap<String,String>();
    		 
 
    		 for(int i=0; i< size; i++){
    			
    			 StringBuffer DirAttr = new StringBuffer("");

    			 DirAttr.append(new File(directoryList.get(i)).getName());
    			 DirAttr.append(new File(directoryList.get(i)).canRead());
    			 DirAttr.append(new File(directoryList.get(i)).canWrite());
    			 DirAttr.append(new File(directoryList.get(i)).canExecute());
    			 DirAttr.append(new File(directoryList.get(i)).isHidden());
    			 DirAttr.append(new File(directoryList.get(i)).lastModified());
    			 
    			 String directoryHashText = DirAttr.toString(); //append dir attributes for SHA-1 calc
    			 
    			// Iterate through the hash table, find the all the children files and directories and append to 
    			// 'directoryHashText'
    			 
    			 Iterator it = SHA_HashTable.entrySet().iterator();
    			 while(it.hasNext()){
    				 Map.Entry pairs = (Map.Entry)it.next();
    				 String fn = (String) pairs.getKey();
    			 
    				//files inside the directory, not including the directory itself
    				 if(fn.startsWith(directoryList.get(i))&& fn != directoryList.get(i))
    				 {
    					// System.out.println("++++++++++" + directList.get(i) + "+++++++++++++");
    					 if(directoryHashText != null)
    						 directoryHashText += (String)pairs.getValue();
    					 else
    						 directoryHashText = (String)pairs.getValue();
    				 
    				 }
    			 
    			 }
    		 
    			 
    		 
    		 // calculate the hash of file hashes for a specific folder
    		 MessageDigest md = MessageDigest.getInstance("SHA1");
    		 
    		 byte[] dataBytes = directoryHashText.getBytes();
 
    			 md.update(dataBytes);
    		 
    		 byte[] mdbytes = md.digest();
 
    		 //convert the byte to hex format
    		 StringBuffer sb = new StringBuffer("");
    		 for (int j = 0; j < mdbytes.length; j++) {
    			 sb.append(Integer.toString((mdbytes[j] & 0xff) + 0x100, 16).substring(1));
    		 }
 
    		 System.out.println("The directory: " + directoryList.get(i) + "------" + "Digest(in hex format):: " + sb.toString());
    
    		 // store the <filepath, hashvalue> pair
    		 Temp_HashTable.put(directoryList.get(i), sb.toString());
    		 }
    	 	
    	 SHA_HashTable.putAll(Temp_HashTable);
    	 }
     }
     
     
     public static void SaveHashToFile(Map<String,String> hm) throws Exception{

    	 		
    			FileOutputStream fos = new FileOutputStream(HashFile);
    			ObjectOutputStream oos = new ObjectOutputStream(fos);
    			oos.writeObject(hm);
    			oos.close();
    			fos.close();

    	}
     
     public static void ReadHashFromFile() throws Exception{
    	 FileInputStream fis = new FileInputStream(HashFile);
    	 ObjectInputStream ois = new ObjectInputStream(fis);
    	 SHA_HashTable = (HashMap<String,String>)ois.readObject();
    	 ois.close();
    	 fis.close();
     }
}
